在第五天 Buzz word 2 : Side Effect 曾經說到會提供另一個很複雜解決 Side Effect 的方法: Effect functor,今天就要來兌現承諾了,也可以統整一下前幾天所學。這邊的 Effect 其實就是前幾天的 Box,你可以自己定義他的名字。
(↑ 嗨,我又出現了之 Box data type 圖)
const Effect = f => ({
map: g => Effect( x => g(f(x)) ),
runEffects: x => f(x) // 拿出來看看裡面是啥
});
Effect 這個 Functor 是把 function 當成參數,這很棒因為我們會想把導致 Side Effect 例如 Math.random()
、 console.log
放到函式裡延後執行
以下模擬一個 web application,這個 application 需要根據不同瀏覽器跟使用者地區顯示不同使用者的資訊及簡介,為了方便起見這些資料被存進了 window
config ,若今天我們想從 window.myAppConf
get 相對應 select element 然後抓 DOM 裡面值
// 想抓這邊的值
window.myAppConf = {
selectors: {
'user-bio': '.userbio',
'article-list': '#articles',
'user-name': '.userfullname',
},
templates: {
'greet': 'Pleased to meet you, {name}',
'notify': 'You have {n} alerts',
}
};
// html
<div class="userbio">Functor is hard</div>
一行結束非常簡潔
document.querySelector(window.myAppConf['user-bio']).innerHTML
但幾乎全部都是 Side Effect,這邊可以列出下列三個 Side Effect
window.myAppConf['user-bio'])
: 抓 function scope 外的值document.querySelector
: 操作 DOMinnerHTML
: 讀取 DOM 裡的值以下就要根據這三個 Side Effect 使用 Functor 寫法做改進
我們可以寫一個 Effect data type 並給予 of
方法,這樣他回傳的就會是 Effect 這個 data type 而不是直接傳 value 丟 Side Effect 出來
// of :: a -> Effect a
Effect.of = val => Effect(() => val);
window.myAppConf['user-bio'])
const win = Effect.of(window); // return 另一個 Effect
const userBioLocator = win.map(x => x.myAppConf.selectors['user-bio']);
// Effect('.userbio')
document.querySelector
再來想要用 document.querySelector()
去抓 DOM ,恩另一個 Side Effect,所以一樣用 Effect.of
把他包成 Pure 的,這樣回傳也會是一個 Effect
// $ :: String -> Effect DOMElement
function $(selector) {
return Effect.of(document.querySelector(selector));
}
若想要結合以上兩個,可以用 map()
const userBio = userBioLocator.map($);
// Effect(Effect(<div>))
innerHTML
好啦現在終於可以抓 DOM 裡面值了
const innerHTML = userBio.map(eff => eff.map(domEl => domEl.innerHTML));
// Effect(Effect('<h2>User Biography</h2>'))
連再一起寫會是
const Effect = f => ({
map: g => Box( x => g(f(x)) ),
runEffects: x => f(x) // 拿出來看看裡面是啥
});
Effect.of = val => Effect(() => val);
function $(selector) {
return Effect.of(document.querySelector(selector));
}
Effect
.of(window)
.map(x => x.myAppConf.selectors['user-bio']);
.map($)
.map(eff => eff.map(domEl => domEl.innerHTML))
// Effect(Effect('<h2>User Biography</h2>'))
崩潰狀,已經出現巢狀 Effect 了,明明以前一行結束的東西,現在為了避免 SIde Effect 變如此複雜,其實是可以寫更好的
// Effect :: Function -> Effect
const Effect = f => ({
map: g => Effect( x => g(f(x)) ),
runEffects: x => f(x),
flatMap: x => f(x)
}
const userBioHTML = Effect.of(window)
.map(x => x.myAppConf.selectors['user-bio']) // Effect('.userbio')
.map($) // // Effect(Effect(<div>))
.flatMap() // Effect(<div>)
.map(x => x.innerHTML); // Effect('<h2>User Biography</h2>')
.runEffects() // Functor IS HARD
藉由 flatMap 可以扁平化,讓寫起來更精簡一點
Chain 就是 map + flatMap
// Effect :: Function -> Effect
const Effect = f => ({
map: g => Effect( x => g(f(x)) ),
runEffects: x => f(x),
flatMap: x => f(x),
chain: g => Effect(f).map(g).flatMap()
}
可以變成更精簡
const userBioHTML = Effect.of(window)
.map(x => x.myAppConf.selectors['user-bio'])
.chain($)
.map(x => x.innerHTML);
// ← Effect('<h2>User Biography</h2>')
完整程式碼 codepen
如有錯誤或需要改進的地方,拜託跟我說。
我會以最快速度修改,感謝您
歡迎追蹤我的部落格,除了技術文也會分享一些在矽谷工作的甘苦。
無意間發現有些地方有漏掉一些字,像是
// Effect :: Function -> Effect
const Effect = f => ({
map: g => Effect( x => g(f(x)) ),
unEffects: x => f(x),
flatMap: x => f(x)
}
這裡應該是
// Effect :: Function -> Effect
const Effect = f => ({
map: g => Effect( x => g(f(x)) ),
runEffects: x => f(x),
flatMap: x => f(x)
}
在下面的有漏掉一樣的地方
// Effect :: Function -> Effect
const Effect = f => ({
map: g => Effect( x => g(f(x)) ),
unEffects: x => f(x),
flatMap: x => f(x),
chain: g => Effect(f).map(g).flatMap()
}
感謝糾正,已經改囉
Functor 好難,看範例就頭昏眼花了,還真不知道怎麼導入專案XD、妳真厲害!
但有發現一個奇怪的地方:
const Effect = f => ({
map: g => Box( x => g(f(x)) ), // <- 這裡應該是要回傳 Effect Type 才對
runEffects: x => f(x)
});
Effect.of = val => Effect(() => val);
function $(selector) {
return Effect.of(document.querySelector(selector));
}
Effect
.of(window)
.map(x => x.myAppConf.selectors['user-bio']);
.map($)
.map(eff => eff.map(domEl => domEl.innerHTML))
// Effect(Effect('<h2>User Biography</h2>'))